#include "attr.h"
#include "chk_error.h"
#include "fibthread.h"
#include "list.h"
#include "sched.h"
#include "asm.h"
#include "debug.h"

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

/* Parametered macro definitions */
	
/* Memory allocation */

#define ALLOC_MEM(type)			((type *)malloc(sizeof(type)))

/* Global data members */

fibthread_struct *curr_thread;
u32_t thread_counter;
u32_t active_thread_counter;
long page_size;

doubly_list priority_queues[FIBTHREAD_REAL_SCHED_PRIO_MAX];
singly_list detached_threads; /* Threads that are finished and in detached state will be appended here */

/* Static data members */

static fibthread_t id_counter;
static u8_t initialized = 0;
static int tmp_counter = 0;

/*
 * all_threads içinde hortlak thread'ler saklanmıyor.
 */

static doubly_list all_threads[ALL_THREADS_BUCKET_CNT];
static doubly_list zombie_threads[ALL_THREADS_BUCKET_CNT];
static doubly_list join_threads[ALL_THREADS_BUCKET_CNT]; /* Threads that are waiting other threads to finish  */

/* Assembly function declarations */

/* Internal function prototypes */
static void dump_registers(fibthread_struct *fibthread);
static int allocate_stack(fibthread_struct *fibthread);
static int allocate_registers(fibthread_struct *fibthread);
static void default_start(void);

static fibthread_struct *get_fibthread_struct_from_zombie_id(fibthread_t id);

static void err_sys(const char *func);

/* External function definitions */

/* 
 * thread ve start_routine parametrelerinin anlamlı değerler olarak girilmesi zorunlu.
 * NULL değer geçilmesi tanımsız davranışa yol açıyor.
 */

int fibthread_create(fibthread_t *thread, const fibthread_attr_t *attr, void *(*start_routine)(void*), void *arg)
{
	int result;
	fibthread_struct *fibthread;
	
	if (initialize())
		return EAGAIN;
	
	if (attr && attr->initialized != FIBTHREAD_ATTR_INITIALIZED)
		return EPERM;
	
	if ((fibthread = ALLOC_MEM(fibthread_struct)) == NULL)
		return EAGAIN;
	
	fibthread->thread_errno = 0;
	fibthread->start_routine = start_routine;
	fibthread->arg = arg;
	fibthread->id = id_counter++;					/* tavan değere ulaştıktan sonra nasıl müdahale edilecek? */
	
	/* Set the join thread to NULL at first */
	fibthread->join_thread = NULL;

	if (attr)
		fibthread->attr = *attr; /* Do not assing directly all the structure you will overwrite stack !!! ==> naah */
	else
		fibthread_attr_init(&fibthread->attr);
	
	/*	at that stage allocate fibers stack and initialize it properly !
			TODO : look for thread_stack_size in attrs ?
	*/
	if((result = allocate_stack(fibthread)))
		return result;

	//printf("-- BEFORE LOAD -- ID : %d\n",fibthread->id);
	//dump_registers(fibthread);
	if(load_registers_asm(&(fibthread->registers)))
		return EAGAIN;
	//printf("-- AFTER LOAD -- ID : %d\n",fibthread->id);
	//dump_registers(fibthread);

	/* According to POSIX standarts on create new thread inherits the the mask of its parent */
	fibthread->thread_mask = curr_thread->thread_mask;
	sigemptyset(&fibthread->pending_signals);

	DOUBLY_LIST_PUSH_BACK(&all_threads[fibthread->id % ALL_THREADS_BUCKET_CNT], &fibthread->all_threads);
	DOUBLY_LIST_INITIALIZE(&curr_thread->zombie_node);
	DOUBLY_LIST_INITIALIZE(&curr_thread->join_node);
	SINGLY_LIST_INITIALIZE(&curr_thread->detach_node);
	DOUBLY_LIST_PUSH_BACK(&priority_queues[fibthread->attr.sched_param.sched_priority], &fibthread->queue_node);
	
	++thread_counter;
	++active_thread_counter;
	
	if(thread)
		*thread = fibthread->id;
			
	return 0;
}

void fibthread_exit(void *value_ptr)
{
	fibthread_struct *prev_fibthread, *next_fibthread;
	
	curr_thread->thread_errno = 0;
		
	curr_thread->return_value_ptr = value_ptr;
	curr_thread->start_routine = NULL;
	curr_thread->arg = NULL;
  
	LOG_INFO(("In fibthread_exit ...\n"));
	DOUBLY_LIST_DELETE_NODE(&curr_thread->all_threads);
	/* At that point we should check to which list we should push the finished thread */
	if(curr_thread->attr.detach_state == FIBTHREAD_CREATE_JOINABLE){
		DOUBLY_LIST_PUSH_FRONT(&zombie_threads[curr_thread->id % ALL_THREADS_BUCKET_CNT], &curr_thread->zombie_node);
		LOG_INFO(("Pushed thread to zombie_list %d \n",curr_thread->id));
		/* Now check if someone is waiting for you */
		if(curr_thread->join_thread != NULL){
			/* Move from join list to ready queue */
			DOUBLY_LIST_DELETE_NODE(&curr_thread->join_thread->join_node);
			DOUBLY_LIST_PUSH_BACK(&priority_queues[curr_thread->join_thread->attr.sched_param.sched_priority], &curr_thread->join_thread->queue_node);
			LOG_INFO(("Removed thread from join list and pushed to priority_list %d \n",curr_thread->join_thread->id));
		}
	}
	else{
		//will change to push it to the detached_threads !
		SINGLY_LIST_PUSH_FRONT(&detached_threads,&curr_thread->detach_node);
		LOG_INFO(("Pushed thread to detached list %d \n",curr_thread->id));
  }
	--thread_counter;
	--active_thread_counter;
	
	if (thread_counter == 0)
		exit((u32_t)value_ptr);
	
	prev_fibthread = curr_thread;
	next_fibthread = pick_thread();
	LOG_DEBUG(("Next fibthread == NULL : %d \n",next_fibthread==NULL));
	LOG_DEBUG(("Next running thread is : %d \n",next_fibthread->id));
	run_thread(next_fibthread);
	curr_thread = next_fibthread;
	
	switch_context(prev_fibthread, next_fibthread);
}

int fibthread_equal(fibthread_t t1, fibthread_t t2)
{
	fibthread_struct *fibthread1, *fibthread2;
	
	if ((fibthread1 = get_fibthread_struct_from_id(t1)) &&
		(fibthread2 = get_fibthread_struct_from_id(t2)))
		return fibthread1->id == fibthread2->id;
	
	return -1;
}

fibthread_t fibthread_self(void)
{
  initialize();	
	return curr_thread->id;
}

int fibthread_yield(void)
{
	++tmp_counter;
	LOG_INFO(("SWITCH_COUNTER : %d \n",tmp_counter));
	if(schedule() != 0)
		return EINVAL;

	return 0;
}

void fibthread_loop(void){

	while(active_thread_counter > 1){

		fibthread_yield();
		LOG_INFO(("In main thread now ! \n"));
		LOG_INFO(("The active counter now is : %d\n",active_thread_counter));
	}
}

/* Wait for a thread to finish its work (posix compliant) */
int fibthread_join(fibthread_t thread, void **value_ptr){
	fibthread_struct *fibthread, *prev_fibthread, *next_fibthread;
  
  LOG_INFO(("Start pthread_join \n"));
  LOG_INFO(("Im thread : %d and Joining thread : %d\n",curr_thread->id,thread));
	/* First check if the arguments are valid */
	if(curr_thread->id == thread)
		return EDEADLK;

	/* Search into zombie list for it */
	if((fibthread = get_fibthread_struct_from_zombie_id(thread)) != NULL){
    LOG_INFO(("The thread is in zombie list so get the result \n"));
		DOUBLY_LIST_DELETE_NODE(&fibthread->zombie_node);
		if(value_ptr)
			*value_ptr = fibthread->return_value_ptr;
		/* At that stage we should free the resources of the thread */
		release_thread_resources(fibthread);

		return 0;
	}
	/* Search for the thread in allthreads first if it is here it is not
	 * finished yet then check for other conditions if they are ok */
	else if((fibthread = get_fibthread_struct_from_id(thread)) != NULL){
		/* At that stage we should check the validity of that thread */
		/* First check if you are the first one that wants to join it*/
    LOG_INFO(("The thread is in allthreads list which means is not finished wait!\n"));
		if(fibthread->join_thread)
			return EINVAL;

		/* Check if it is a joinable thread if not exit again */
		if(fibthread->attr.detach_state != FIBTHREAD_CREATE_JOINABLE)
			return EINVAL;
	}

	else
		return ESRCH; /* That means that thread id is not a valid one */

	/* At that stage we have a thread which is valid to be joined
	 * Now firstly mark that one as a joined one so other theads wont
	 * mess up with it ! */
	fibthread->join_thread = curr_thread;

	/* Put yourself into join_threads list and go to sleep */
	if (active_thread_counter == 1)
			return EAGAIN;

	prev_fibthread = curr_thread;
	next_fibthread = pick_thread();

	/* Put the current thread into join_threads list */
	DOUBLY_LIST_PUSH_FRONT(&join_threads[prev_fibthread->id % ALL_THREADS_BUCKET_CNT], &prev_fibthread->join_node);
	run_thread(next_fibthread);
	curr_thread = next_fibthread;
	/* Fall in sleep till your join thread comes to save you !*/
  LOG_INFO(("Falling asleep till my join thread finishes !\n"));
	switch_context(prev_fibthread, next_fibthread);

	/*
	 * When we come back we should again check if our thread is really in zombie list
	 * if it really finished ? Maybe we got some error (Not very efficient but ) join
	 * is not something very efficient anyway !
	 *
	 */
  LOG_INFO(("I was wakened up and start releasing old resources for thread!\n"));
	if((fibthread = get_fibthread_struct_from_zombie_id(thread)) != NULL){
		/* Remove it from the zombie list */
		LOG_INFO(("BEFORE Thread deleted from zombie list\n"));
		DOUBLY_LIST_DELETE_NODE(&fibthread->zombie_node);
		LOG_INFO(("Thread deleted from zombie list\n"));
		/* Get the Result */
		if(value_ptr)
			*value_ptr = fibthread->return_value_ptr;
		/* At that stage we should free the resources of the thread */
		LOG_INFO(("Before releasing zombie thread\n"));
		release_thread_resources(fibthread);
		LOG_INFO(("After releasing zombie thread\n"));

		return 0;
	}
	else return EINVAL; /* That shouldnt happen really */

	return 0;
}

/* Functions that are used between internal API of fibthread */

/* Place where you will be releasing the resources of the fibers
 * At that stage we assume that thread to be released is delete from
 * all of the lists in the system ! ... */

void release_thread_resources(fibthread_struct *fibthread)
{
	char *real_bottom_addr;

	real_bottom_addr = ((char *)fibthread->bottom_stack) - fibthread->attr.guard_size + 4;
	munmap((void *)real_bottom_addr, fibthread->attr.guard_size + fibthread->attr.stack_size);

	/* Release all the data structure finally */
	free(fibthread);
}

fibthread_struct *get_fibthread_struct_from_id(fibthread_t id)
{
	doubly_list *dlist;
	doubly_node *tmp_pos;
	fibthread_struct *fibthread;

	dlist = &all_threads[id % ALL_THREADS_BUCKET_CNT];

	if (IS_LIST_EMPTY(dlist))
		return NULL;

	WALK_LIST_FWD(dlist, tmp_pos) {
		fibthread = EXTRACT_OBJECT(fibthread_struct, all_threads, tmp_pos);

		if (fibthread->id == id)
			return fibthread;
	}

	return NULL;
}

int initialize(void)
{
	int i,result;
	
	if (initialized)
		return 0;
	
	/*
	 * İçsel veri yapıları başlatılıyor
	 */
	
	/*
	 * "0" geçerli bir id değeri değil.
	 */

	id_counter = 1;
	thread_counter = 0;
	active_thread_counter = 0;

#if defined(PAGESIZE) || defined(PAGE_SIZE)
#if defined(PAGESIZE)
	page_size = PAGESIZE;
#else
	page_size = PAGE_SIZE;
#endif
#elif defined(_SC_PAGESIZE) || defined(_SC_PAGE_SIZE)
	errno = 0;

#if defined(_SC_PAGESIZE)
	page_size = sysconf(_SC_PAGESIZE);
#else
	page_size = sysconf(_SC_PAGE_SIZE);
#endif

	chk_errno("sysconf", errno);
#else
#define PAGE_SIZE_GUESS		4096

	page_size = PAGE_SIZE_GUESS;
#endif
	
	for (i = 0; i < FIBTHREAD_THREADS_MAX / 4; ++i) {
		DOUBLY_LIST_INITIALIZE(&all_threads[i]);
		DOUBLY_LIST_INITIALIZE(&zombie_threads[i]);
		DOUBLY_LIST_INITIALIZE(&join_threads[i]);
	}
	for (i = 0; i < FIBTHREAD_REAL_SCHED_PRIO_MAX; ++i)
		DOUBLY_LIST_INITIALIZE(&priority_queues[i]);
	
	/* Initialize detached list of threads */
	SINGLY_LIST_INITIALIZE(&detached_threads);

	/*
	 * Mevcut akış kaydediliyor
	 */
	
	/** Çökmesi muhtemel kısımları başa alalım.. **/
	if ((curr_thread = ALLOC_MEM(fibthread_struct)) == NULL)
		return ENOMEM;

	curr_thread->thread_errno = errno;
	
	curr_thread->start_routine = NULL;
	curr_thread->arg = NULL;
	
	curr_thread->id = id_counter++;
	curr_thread->attr.sched_policy = FIBTHREAD_SCHED_POLICY_DEFAULT;
	curr_thread->attr.sched_param.sched_priority = FIBTHREAD_SCHED_PRIORITY_DEFAULT;

	/* some stack initialization here ! We dont allocate stack for main thread
	   because it is already allocated via main Process
	*/
	curr_thread->bottom_stack = NULL;
	curr_thread->stack_addr = NULL;

	// initialize the registers of the main thread !
	if((result = allocate_registers(curr_thread)) != 0)
		return result;

	/* initialize the signal mask for the main thread */
	if((result = sigprocmask(SIG_BLOCK, NULL, &curr_thread->thread_mask)) < 0)
		return result;
	sigemptyset(&curr_thread->pending_signals);

	DOUBLY_LIST_PUSH_FRONT(&all_threads[curr_thread->id%ALL_THREADS_BUCKET_CNT], &curr_thread->all_threads);
	
	++thread_counter;
	++active_thread_counter;

	initialized = 1;
	
	return 0;
}


/* Internal function definitions */
/*
 * We will be allocating place here for register values
 * that are the snapshot of current thread
 *
 *  -------0000
 *	EIP
 *	-------
 *	EBP
 *	-------
 *	EBX
 *	-------
 *	ESI
 *	-------
 *	EDI
 *	-------
 *	ECX
 *	-------
 *	EDX
 *	-------
 *	FS
 *	-------FFFF
 *
 */


/*
 * İsmini mi değiştirsek? 
 */

static int allocate_registers(fibthread_struct *fibthread){
	fibthread->registers.eip = &default_start;

	fibthread->registers.ebp = NULL;
	fibthread->registers.ebx = NULL;
	fibthread->registers.esi = NULL;
	fibthread->registers.edi = NULL;
	fibthread->registers.ecx = NULL;
	fibthread->registers.edx = NULL;
	fibthread->registers.fs = NULL;

	return 0;
}

static int allocate_stack(fibthread_struct *fibthread)
{
	size_t stack_size, guard_size;
	char *stack_addr;				/* Lowest addressable byte */			/** Biz de tam tersi.. **/

	if (fibthread->attr.stack_addr) { /* User defined stack.. */
		fibthread->bottom_stack = fibthread->attr.stack_addr;
		fibthread->stack_addr = (char *)fibthread->attr.stack_addr + fibthread->attr.stack_size;
		return 0;
	}

	stack_size = fibthread->attr.stack_size;
	guard_size = fibthread->attr.guard_size;

	stack_size += stack_size % page_size;
	guard_size += guard_size % page_size;

	if ((stack_addr = (char *)mmap(NULL, stack_size + guard_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)) == MAP_FAILED)
		return ENOMEM;

	fibthread->stack_addr = stack_addr + stack_size + guard_size - 4;
	fibthread->bottom_stack = stack_addr + guard_size - 4;

	if (guard_size != 0)
		fibthread->stack_guard = (short *)((char *)fibthread->bottom_stack - 4), *fibthread->stack_guard = 0xDEAD;
	else
		fibthread->stack_guard = NULL;

	allocate_registers(fibthread);

	return 0;
}

static void default_start(void) {
	void *return_adress;	
	LOG_DEBUG(("START :\n"));
	if(curr_thread->start_routine == NULL ){
		LOG_FATAL(("NULL value for start_routine ? \n"));
		err_sys("NULL value for start_routine ? \n");
	}
	else{
		return_adress = curr_thread->start_routine(curr_thread->arg);
	}
	LOG_DEBUG(("CONTINUE\n"));
	fibthread_exit(return_adress);
	
	/*we should never return here !*/
	abort();
}

static void dump_registers(fibthread_struct *fibthread){

	LOG_DEBUG(("EIP : %p \n",fibthread->registers.eip));
	LOG_DEBUG(("EBP : %p \n",fibthread->registers.ebp));
	LOG_DEBUG(("EBX : %p \n",fibthread->registers.ebx));
	LOG_DEBUG(("ESI : %p \n",fibthread->registers.esi));
	LOG_DEBUG(("EDI : %p \n",fibthread->registers.edi));
	LOG_DEBUG(("ECX : %p \n",fibthread->registers.ecx));
	LOG_DEBUG(("EDX : %p \n",fibthread->registers.edx));
	LOG_DEBUG(("FS : %p \n",fibthread->registers.fs));

}

static fibthread_struct *get_fibthread_struct_from_zombie_id(fibthread_t id)
{
	doubly_list *dlist;
	doubly_node *tmp_pos;
	fibthread_struct *fibthread;

	dlist = &zombie_threads[id % ALL_THREADS_BUCKET_CNT];

	if (IS_LIST_EMPTY(dlist))
		return NULL;

	WALK_LIST_FWD(dlist, tmp_pos) {
		fibthread = EXTRACT_OBJECT(fibthread_struct, zombie_node, tmp_pos);

		if (fibthread->id == id)
			return fibthread;
	}

	return NULL;
}

static void err_sys(const char *func)
{
	fprintf(stderr, "%s: %s\n", func, strerror(errno));
	exit(EXIT_FAILURE);
}
